package com.yuyaogc.lowcode.engine.plugin.activerecord;

import com.yuyaogc.lowcode.engine.annotation.Table;
import com.yuyaogc.lowcode.engine.container.Constants;
import com.yuyaogc.lowcode.engine.container.Container;
import com.yuyaogc.lowcode.engine.context.Context;
import com.yuyaogc.lowcode.engine.entity.EntityClass;
import com.yuyaogc.lowcode.engine.entity.EntityField;
import com.yuyaogc.lowcode.engine.entity.datatype.DataType;
import com.yuyaogc.lowcode.engine.util.ArrayUtils;
import com.yuyaogc.lowcode.engine.util.Memory;
import com.yuyaogc.lowcode.engine.util.ModelUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.*;
import java.util.*;

public class DbUtil {
    public static final Logger log = LoggerFactory.getLogger(DbUtil.class);
    public static String quote(String identify) {
        return String.format("`%s`", identify);
    }
    public static List<String> qualify(Query query, EntityField field) {
        List<String> relColumns = new ArrayList<>();

        EntityClass rec = field.getEntity();
        if (field.getDataType() instanceof DataType.Many2oneField) {
            DataType.Many2oneField m2o = (DataType.Many2oneField) field.getDataType();
            String relModel = field.getRelModel();
            EntityClass relClass = Container.me().getEntityClass(relModel);
            String aliasRel = query.leftJoin(rec.getTableName(), field.getColumnName(), relClass.getTableName(), "id", field.getColumnName());

            for (EntityField relField : relClass.getFields()) {
                if (relField.getName().equals("id")) {
                    continue;
                }
                if (!relField.isStore()) {
                    continue;
                }
                String alisColumn = String.format("%s.%s", aliasRel, quote(relField.getColumnName()));
                relColumns.add(String.format("%s as %s", alisColumn, quote(relField.getName())));
            }
            //TODO
            return relColumns;
        }

        return null;
    }


    public static List<String> generateOrderByInner(Config cr,EntityClass rec, String alias, String orderSpec, Query query,
                                              boolean reverseDirection, Set<String> seen) {
        if (seen == null) {
            seen = new HashSet<>();
        }
        // TODO
        List<String> orderByElements = new ArrayList<>();
        for (String orderPart : orderSpec.split(",")) {
            String[] orderSplit = orderPart.trim().split(" ");
            String orderField = orderSplit[0].trim();
            String orderDirection = orderSplit.length == 2 ? orderSplit[1].trim().toLowerCase() : "";
            if (reverseDirection) {
                orderDirection = "DESC".equals(orderDirection) ? "ASC" : "DESC";
            }
            boolean doReverse = "DESC".equals(orderDirection);
            EntityField field = rec.getField(orderField);
            if (Constants.ID.equals(orderField)) {
                orderByElements
                        .add(String.format("%s.%s %s", cr.quote(alias), cr.quote(orderField), orderDirection));
            } else {
                if (field.isStore() && "many2one".equals(field.getDataType())) {
                    DataType.Many2oneField m2o = (DataType.Many2oneField) field.getDataType();
                } else if (field.isStore() && field.getDataType().getType() != ColumnType.None) {
                } else {
                    log.warn("模型{}不能按字段{}排序", rec.getName(), orderField);
                }
            }
        }
        return orderByElements;
    }
    public static String generateOrderBy(Config cr, EntityClass rec, String orderSpec, Query query) {
        String orderByClause = "";
        if (org.apache.commons.lang3.StringUtils.isBlank(orderSpec)) {
            orderSpec = rec.getOrder();
        }
        if (org.apache.commons.lang3.StringUtils.isNotBlank(orderSpec)) {
            List<String> orderByElements = generateOrderByInner(cr,rec, rec.getTableName(), orderSpec, query,
                    false, null);
            if (!orderByElements.isEmpty()) {
                orderByClause = org.apache.commons.lang3.StringUtils.join(orderByElements);
            }
        }
        return orderByClause;
    }
    public static Query whereCalc(Config config, EntityClass rec, Criteria criteria, boolean activeTest) {
        // TODO active criteria
        if (!criteria.isEmpty()) {
            return new Expression(config, criteria, rec, null, null).getQuery();
        } else {
            return new Query(config, rec.getTableName(), rec.getTableName());
        }
    }

    public static Class<? extends Model> _getModelClass(Class<? extends Model> clazz) {
        if (clazz.isAnnotationPresent(Table.class)) {
            //TODO app name?

            Table table = (Table) clazz.getAnnotation(Table.class);
            Context.getInstance().get(table.name());
        }
        return clazz;
    }

    public static long count(Class<? extends Model> type, Config cr,Criteria criteria) {
        _getModelClass(type);
        EntityClass rec = Context.getInstance().getEntityClass();
        if (Expression.isFalse(rec, criteria)) {
            return 0;
        }
        Query query = whereCalc(cr, rec, criteria, true);
        Connection conn;

        try {
            conn = cr.getConnection();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        Query.SelectClause select = query.select("count(1)");

        SqlPara format = cr.mogrify(select.getQuery(), select.getParams());

        try (PreparedStatement pst = conn.prepareStatement(format.getSql())) {
            cr.dialect.fillStatement(pst, format.getParmas());
            ResultSet rs = pst.executeQuery();
            int row = 0;
            if (rs.next()) {
                row = rs.getInt(1);
            }
            DbUtil.close(rs);
            return row;
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {

            cr.close(conn);
        }
        return 0;
    }

    public static <T extends Model> List<T> search(Class<? extends Model> type, Config config, Criteria criteria, Integer offset, Integer limit, String order){
        try {
            _getModelClass(type);
            EntityClass rec = Context.getInstance().getEntityClass();
            Query query = whereCalc(config, rec, criteria, false);
            query.setOrder(generateOrderBy(config, rec, order, query)).setLimit(limit).setOffset(offset);

            Set<String> columns = new HashSet<>();
            for (EntityField field : rec.getFields()) {
                if (!field.isStore()) {
                    continue;
                }

                String alisColumn = String.format("%s.%s", config.quote(rec.getTableName()), config.quote(field.getColumnName()));
                columns.add(String.format("%s as %s", alisColumn, config.quote(field.getName())));
                List<String> relColumns = qualify(query, field);
                if (Objects.isNull(relColumns)) {
                    continue;
                }

                columns.addAll(relColumns);
            }

            Query.SelectClause select = query.select(columns);
            Connection connection = config.getConnection();
            SqlPara format = config.mogrify(select.getQuery(), select.getParams());

            //TODO DEBUG
            log.info(format.getSql());


            try (PreparedStatement pst = connection.prepareStatement(format.getSql())) {
                config.dialect.fillStatement(pst, format.getParmas());
                ResultSet rs = pst.executeQuery();
                List<T> result = config.dialect.buildModelList(rs, _getModelClass(type));


                List<Object> ids = new ArrayList<>();
                if (!result.isEmpty()) {

                    Map<String, List<Object>> col = ModelUtil.convertListToMap(result);

                    ids = col.get("id");

                    Memory memory = Context.getInstance().getCache();
                    for (EntityField field : rec.getFields()) {
                        List<Object> values = col.get(field.getName());
                        if (null == values) {
                            continue;
                        }
                        memory.update(ids.toArray(ArrayUtils.EMPTY_LONG_OBJECT_ARRAY), field, values);
                    }

                    for (EntityField field : rec.getFields()) {
                        //TODO Many2many
                        field.getDataType().read(field, query, ids.toArray(ArrayUtils.EMPTY_LONG_OBJECT_ARRAY));
                    }
                }


                // TODO
                List data = new ArrayList<>();
                for (Object id : ids) {
                    Model<?> vals = _getModelClass(type).newInstance();
                    Memory cache = Context.getInstance().getCache();
                    //Map<String,Object> vals = new LinkedHashMap<>();
                    vals.put(Constants.ID, id);
                    for (EntityField field : rec.getFields()) {
                        //vals.put(field.getName(), null);
                        Object value = cache.get((Long) id, field, Void.class);
                        if (value == Void.class) {

                        }
                        vals.put(field.getName(), value);
                    }
                    data.add(vals);
                }


                DbUtil.close(rs);
                return data;
            } catch (Exception e) {
                throw new ActiveRecordException(e);
            } finally {
                config.close(connection);
            }

        } catch (Exception e) {
            throw new ActiveRecordException(e);
        }

    }

    public static int update(Config config, Connection conn, String sql, List<Object> paras) throws SQLException {
        try (PreparedStatement pst = conn.prepareStatement(sql)) {
            config.dialect.fillStatement(pst, paras);
            int result = pst.executeUpdate();
            return result;
        }
    }

    public static int update(Config config, Connection conn, String sql, Object... paras) throws SQLException {
        try (PreparedStatement pst = conn.prepareStatement(sql)) {
            config.dialect.fillStatement(pst, paras);
            int result = pst.executeUpdate();
            return result;
        }
    }

    static final void close(ResultSet rs, Statement st) throws SQLException {
        if (rs != null) {rs.close();}
        if (st != null) {st.close();}
    }

    static final void close(ResultSet rs) throws SQLException {
        if (rs != null) {rs.close();}
    }

    static final void close(Statement st) throws SQLException {
        if (st != null) {st.close();}
    }
}
